/*
 * Copyright 2009 Guenther Niess
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kenai.jbosh;

import java.io.IOException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;

import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

final class ApacheHTTPResponse implements HTTPResponse {

	// /////////////////////////////////////////////////////////////////////////
	// Constants:

	/**
	 * Name of the accept encoding header.
	 */
	private static final String ACCEPT_ENCODING = "Accept-Encoding";

	/**
	 * Value to use for the ACCEPT_ENCODING header.
	 */
	private static final String ACCEPT_ENCODING_VAL = ZLIBCodec.getID() + ", "
			+ GZIPCodec.getID();

	/**
	 * Name of the character set to encode the body to/from.
	 */
	private static final String CHARSET = "UTF-8";

	/**
	 * Content type to use when transmitting the body data.
	 */
	private static final String CONTENT_TYPE = "text/xml; charset=utf-8";

	// /////////////////////////////////////////////////////////////////////////
	// Class variables:

	/**
	 * Lock used for internal synchronization.
	 */
	private final Lock lock = new ReentrantLock();

	/**
	 * The execution state of an HTTP process.
	 */
	private final HttpContext context;

	/**
	 * HttpClient instance to use to communicate.
	 */
	private final HttpClient client;

	/**
	 * The HTTP POST request is sent to the server.
	 */
	private final HttpPost post;

	/**
	 * A flag which indicates if the transmission was already done.
	 */
	private boolean sent;

	/**
	 * Exception to throw when the response data is attempted to be accessed, or
	 * {@code null} if no exception should be thrown.
	 */
	private BOSHException toThrow;

	/**
	 * The response body which was received from the server or {@code null} if
	 * that has not yet happened.
	 */
	private AbstractBody body;

	/**
	 * The HTTP response status code.
	 */
	private int statusCode;

	// /////////////////////////////////////////////////////////////////////////
	// Constructors:

	/**
	 * Create and send a new request to the upstream connection manager,
	 * providing deferred access to the results to be returned.
	 * 
	 * @param client
	 *            client instance to use when sending the request
	 * @param cfg
	 *            client configuration
	 * @param params
	 *            connection manager parameters from the session creation
	 *            response, or {@code null} if the session has not yet been
	 *            established
	 * @param request
	 *            body of the client request
	 */
	ApacheHTTPResponse(final HttpClient client, final BOSHClientConfig cfg,
			final CMSessionParams params, final AbstractBody request) {
		super();
		this.client = client;
		this.context = new BasicHttpContext();
		this.post = new HttpPost(cfg.getURI().toString());
		this.sent = false;

		try {
			String xml = request.toXML();
			byte[] data = xml.getBytes(CHARSET);

			String encoding = null;
			if (cfg.isCompressionEnabled() && params != null) {
				AttrAccept accept = params.getAccept();
				if (accept != null) {
					if (accept.isAccepted(ZLIBCodec.getID())) {
						encoding = ZLIBCodec.getID();
						data = ZLIBCodec.encode(data);
					} else if (accept.isAccepted(GZIPCodec.getID())) {
						encoding = GZIPCodec.getID();
						data = GZIPCodec.encode(data);
					}
				}
			}

			ByteArrayEntity entity = new ByteArrayEntity(data);
			entity.setContentType(CONTENT_TYPE);
			if (encoding != null) {
				entity.setContentEncoding(encoding);
			}
			post.setEntity(entity);
			if (cfg.isCompressionEnabled()) {
				post.setHeader(ACCEPT_ENCODING, ACCEPT_ENCODING_VAL);
			}
		} catch (Exception e) {
			toThrow = new BOSHException("Could not generate request", e);
		}
	}

	// /////////////////////////////////////////////////////////////////////////
	// HTTPResponse interface methods:

	/**
	 * Abort the client transmission and response processing.
	 */
	public void abort() {
		if (post != null) {
			post.abort();
			toThrow = new BOSHException("HTTP request aborted");
		}
	}

	/**
	 * Wait for and then return the response body.
	 * 
	 * @return body of the response
	 * @throws InterruptedException
	 *             if interrupted while awaiting the response
	 * @throws BOSHException
	 *             on communication failure
	 */
	public AbstractBody getBody() throws InterruptedException, BOSHException {
		if (toThrow != null) {
			throw (toThrow);
		}
		lock.lock();
		try {
			if (!sent) {
				awaitResponse();
			}
		} finally {
			lock.unlock();
		}
		return body;
	}

	/**
	 * Wait for and then return the response HTTP status code.
	 * 
	 * @return HTTP status code of the response
	 * @throws InterruptedException
	 *             if interrupted while awaiting the response
	 * @throws BOSHException
	 *             on communication failure
	 */
	public int getHTTPStatus() throws InterruptedException, BOSHException {
		if (toThrow != null) {
			throw (toThrow);
		}
		lock.lock();
		try {
			if (!sent) {
				awaitResponse();
			}
		} finally {
			lock.unlock();
		}
		return statusCode;
	}

	// /////////////////////////////////////////////////////////////////////////
	// Package-private methods:

	/**
	 * Await the response, storing the result in the instance variables of this
	 * class when they arrive.
	 * 
	 * @throws InterruptedException
	 *             if interrupted while awaiting the response
	 * @throws BOSHException
	 *             on communication failure
	 */
	private synchronized void awaitResponse() throws BOSHException {
		HttpEntity entity = null;
		try {
			HttpResponse httpResp = client.execute(post, context);
			entity = httpResp.getEntity();
			byte[] data = EntityUtils.toByteArray(entity);
			String encoding = entity.getContentEncoding() != null ? entity
					.getContentEncoding().getValue() : null;
			if (ZLIBCodec.getID().equalsIgnoreCase(encoding)) {
				data = ZLIBCodec.decode(data);
			} else if (GZIPCodec.getID().equalsIgnoreCase(encoding)) {
				data = GZIPCodec.decode(data);
			}
			body = StaticBody.fromString(new String(data, CHARSET));
			statusCode = httpResp.getStatusLine().getStatusCode();
			sent = true;
		} catch (IOException iox) {
			abort();
			toThrow = new BOSHException("Could not obtain response", iox);
			throw (toThrow);
		} catch (RuntimeException ex) {
			abort();
			throw (ex);
		}
	}
}
